package com.stone.starter.core.script;


import com.stone.starter.core.script.tool.ClassResolver;
import com.stone.starter.core.script.tool.MemoryClassLoader;

import javax.tools.JavaCompiler;
import javax.tools.JavaCompiler.CompilationTask;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;

/**
 * java代码编译，即将 xxx.java源文件编译为字节码文件并缓存起来，
 * 并且提供loadClass方法，该方法可以获取动态编译后的源代码的Class对象
 *
 * @author Mr_wenpan@163.com 2021/9/7 1:15 下午
 */
public class JavaCodeCompiler {

    /**
     * java编译器
     */
    private final JavaCompiler javaCompiler;

    private final StandardJavaFileManager standardJavaFileManager;

    public JavaCodeCompiler() {
        javaCompiler = ToolProvider.getSystemJavaCompiler();
        standardJavaFileManager = javaCompiler.getStandardFileManager
                (null, Locale.getDefault(), Charset.defaultCharset());
    }

    /**
     * 加载.java文件为Class对象
     *
     * @param sourceCode 源码
     * @return java.lang.Class<?> 类的Class对象
     * @author Mr_wenpan@163.com 2021/9/8 9:58 上午
     */
    public Class<?> loadClass(String sourceCode) throws IOException, ClassNotFoundException {
        // 解析源码元信息
        ClassResolver.Result result = ClassResolver.resolver(sourceCode);

        // 一旦类被加载不允许再修改
        synchronized (this) {
            try {
                // 先去尝试加载
                return Class.forName(result.getClassFullName());
            } catch (ClassNotFoundException ex) {
                // 如果内存中加载失败，则手动编译该源码，生成该源码的字节码的二进制内容
                Map<String, byte[]> clazzBytes = compile(result.getClassFileName(), sourceCode);
                // 使用自定义类加载器将字节码的二进制内容加载为类的Class对象
                return loadClass(result.getClassFullName(), clazzBytes);
            }
        }
    }

    /**
     * 编译java代码到内存
     *
     * @param fileName java文件名称 xxx.java
     * @param source   代码内容
     * @return 字节码
     * @throws IOException 异常信息
     */
    private Map<String, byte[]> compile(String fileName, String source) throws IOException {
        // MemoryJavaCodeManager替换JDK默认的StandardJavaFileManager，
        // 以便在编译器请求源码内容时，不是从文件读取，而是直接返回String
        try (MemoryJavaCodeManager manager = new MemoryJavaCodeManager(standardJavaFileManager)) {
            // Java编译器根本不关心源码的内容是从哪来的，你给它一个String当作源码，它就可以输出byte[]作为class的内容
            JavaFileObject javaFileObject = manager.makeStringSource(fileName, source);
            CompilationTask task = javaCompiler.getTask(
                    null,
                    manager,
                    null,
                    null,
                    null,
                    Collections.singletonList(javaFileObject)
            );
            Boolean result = task.call();
            if (result == null || !result) {
                throw new RuntimeException("Compilation failed.");
            }
            return manager.getClassBytes();
        }
    }

    /**
     * 加载字节码文件到jvm中
     *
     * @param name       类名称
     * @param classBytes 类的字节码的二进制内容
     * @return 类
     * @throws ClassNotFoundException 类不存在
     * @throws IOException            io异常
     */
    private Class<?> loadClass(String name, Map<String, byte[]> classBytes) throws ClassNotFoundException, IOException {
        try (MemoryClassLoader classLoader = new MemoryClassLoader(classBytes)) {
            // 通过自定义类加载器加载字节码二进制内容为Class对象
            return classLoader.loadClass(name);
        }
    }
}
